﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web;

namespace Quail
{
    /// <summary>
    /// A callback to finish an AsyncTask.
    /// </summary>
    /// <param name="asyncTask">The <see cref="AsyncTask"/> object invoking the callback.</param>
    public delegate void AsyncTaskCallback(AsyncTask asyncTask);

    /// <summary>
    /// An AsyncTask provides a method of testing long running operations.
    /// </summary>
    public class AsyncTask
    {
        private const LogStatus UnfinishedAsyncStatus = LogStatus.PendingAsync;

        private DateTime _dateCreated = DateTime.UtcNow;
        private LogStatus _status = UnfinishedAsyncStatus; 
        

        /// <summary>
        /// Returns the ID of the AsyncTask test. This ID corresponds to the auto-incremented primary key in the AsyncTask database table.
        /// </summary>
        public int AsyncTaskId { get; set; }


        /// <summary>
        /// Returns the ID of the test that invoked AsyncTask.
        /// </summary>
        public int TestRunId { get; set; }


        /// <summary>
        /// Returns the ID of the task that invoked AsyncTask.
        /// </summary>
        public long TestRunTaskId { get; set; }

        /// <summary>
        /// Returns the name of the AsyncTask test.
        /// </summary>
        public string TestName { get; set; }


        /// <summary>
        /// Returns data which will be compared to future results to determine success/failure of AsyncTask test.
        /// </summary>
        public string TestData { get; set; }


        /// <summary>
        /// Returns DateTime AsyncTask test was created. Should be UTC DateTime.
        /// </summary>
        public DateTime DateCreated
        {
            get { return _dateCreated; }
            set { _dateCreated = value; }
        }


        /// <summary>
        /// Returns DateTime AsyncTask test will expire.
        /// </summary>
        public DateTime DateExpires { get; set; }


        /// <summary>
        /// Returns current status of AsyncTask test.
        /// </summary>
        public LogStatus Status
        {
            get { return _status; }
            set { _status = value; }
        }


        /// <summary>
        /// Helper method determines whether an AsyncTask test has expired.
        /// </summary>
        public bool HasExpired
        {
            get { return DateTime.UtcNow > DateExpires; }
        }


        /// <summary>
        /// Helper method provides URL to parent task in the QuailDashboard.
        /// </summary>
        public string QuailUrl
        {
            get { return BuildQuailUrl(TestRunId); }
        }

        /// <summary>
        /// Gets of sets the name of the callback method to use to finish the async task.
        /// </summary>
        private string CallbackMethodName { get; set; }

        /// <summary>
        /// Gets of sets the name of the class containing the callback method to use to finish the async task.
        /// </summary>
        private string CallbackClassName { get; set; }

        /// <summary>
        /// Gets of sets the name of the assembly containing the callback method to use to finish the async task.
        /// </summary>
        private string CallbackAssemblyName { get; set; }

        private string BuildQuailUrl(int testRunId)
        {
            return string.Format("http://qaquailweb/Dashboard.aspx#/Detail/{0}", testRunId); 
        }

        /// <summary>
        /// Begins an asyncronous task.  Task will finish the parent task.  Callback method will be executed when
        /// the async task sweeper test executes to finish all outstanding async tasks.
        /// </summary>
        /// <param name="parentTask">The parent task for to execute the async task off of.</param>
        /// <param name="callback">Callback method async task sweeper test should call to finish the test.</param>
        /// <param name="metadata">Any data needed by the callback method to properly finish.</param>
        /// <param name="timeOut">The amount of time after which the async task should be considered timed out.</param>
        public static void BeginAsyncTask(Task parentTask, AsyncTaskCallback callback, string metadata, TimeSpan timeOut)
        {
            string callbackName = callback.Method.Name;
            Type testClassType = callback.Target.GetType();
            string testClassName = testClassType.FullName;
            string assemblyname = Assembly.GetAssembly(testClassType).GetName().Name;
            var expires = DateTime.UtcNow.Add(timeOut);
            var asyncTask = new AsyncTask()
            {
                TestName = parentTask.Name,
                TestData = metadata,
                TestRunId = parentTask.TestRunId(),
                TestRunTaskId = parentTask.TestRunTaskId(),
                DateExpires = expires,
                CallbackMethodName = callbackName,
                CallbackClassName = testClassName,
                CallbackAssemblyName = assemblyname
            };
            asyncTask.Insert();
            parentTask.Finish(UnfinishedAsyncStatus, "Async Request Pending");
        }

        /// <summary>
        /// Call the callback function associated with the AsyncTask.
        /// </summary>
        public void InvokeCallback()
        {
            if (!HasExpired)
            {
                MethodInfo callback = null;
                object testClass = null;
                try
                {
                    Assembly assembly = AppDomain.CurrentDomain.GetAssemblies()
                        .FirstOrDefault(a => a.GetName().Name == CallbackAssemblyName);
                    if (assembly == null)
                    {
                        assembly = Assembly.LoadFrom(CallbackAssemblyName + ".dll");
                    }

                    testClass = assembly.CreateInstance(CallbackClassName);
                    callback = testClass.GetType().GetMethod(CallbackMethodName,
                                                             BindingFlags.Public | BindingFlags.NonPublic |
                                                             BindingFlags.Instance | BindingFlags.Static);
                }
                catch(Exception)
                {
                }

                if (callback != null)
                {
                    callback.Invoke(testClass, new object[] {this});
                }
                else
                {
                    Finish(LogStatus.Fail, string.Empty, "Unable to find AsyncTask callback.", 0);
                }
            }
            else
            {
                Finish(LogStatus.FailTimeout, string.Empty, "Async Task expired", 0);
            }
        }

        /// <summary>
        /// Set in-context AsyncTask object status and update parent task status and message.
        /// </summary>
        /// <param name="status">LogStatus.* representing AsyncTask record status.</param>
        /// <param name="url">URL of task running. Get this from Task.TestSession.</param>
        /// <param name="message">Message saved to parent task.</param>
        /// <param name="associatedTestRunId">Id of the finishing task to provide link in starting task.</param>
        public void Finish(LogStatus status, string url, string message, int associatedTestRunId)
        {
            // Save child task (AsyncTask).
            Status = status;
            Save();

            // And then update parent task.
            UpdateParentTasks(url, message, associatedTestRunId);
        }

        /// <summary>
        /// Get records from AsyncTask table with pending status id in the given environment.
        /// </summary>
        /// <param name="environmentId">The id of the environment to retrieve tasks for.</param>
        /// <returns>A list of pending <see cref="AsyncTask"/>s.</returns>
        public static List<AsyncTask> GetAllPending(int environmentId)
        {
            var listOfAsyncTasks = new List<AsyncTask>();

            try
            {
                using (var client = QuailServices.QuailDataServiceFactory.CreateClient())
                {
                    QuailServices.QuailDataServiceReference.AsyncTask[] asyncTasks =
                        client.GetAllAsyncTasks((int) UnfinishedAsyncStatus, environmentId);
                    foreach (var asyncTask in asyncTasks)
                    {
                        var task = new AsyncTask()
                        {
                            AsyncTaskId = asyncTask.AsyncTaskId,
                            TestRunId = asyncTask.TestRunId,
                            TestRunTaskId = asyncTask.TestRunTaskId,
                            TestName = asyncTask.TestName,
                            TestData = asyncTask.TestData,
                            Status = (LogStatus)asyncTask.StatusId,
                            DateCreated = asyncTask.DateCreated,
                            DateExpires = asyncTask.DateExpires,
                            CallbackMethodName = asyncTask.CallbackMethodName,
                            CallbackClassName = asyncTask.CallbackClassName,
                            CallbackAssemblyName = asyncTask.CallbackAssemblyName
                        };

                        listOfAsyncTasks.Add(task);
                    }
                }
            }
            catch (Exception)
            {
                // TODO: How are we handling exceptions here?
            }

            return listOfAsyncTasks;
        }


        /// <summary>
        /// Inserts the in-context AsyncTask object to the database.
        /// </summary>
        private int Insert()
        {
            var id = 0;
            try
            {
                using (var client = QuailServices.QuailDataServiceFactory.CreateClient())
                {
                    id = client.InsertAsyncTask(
                        new QuailServices.QuailDataServiceReference.AsyncTask()
                        {
                            TestRunId = TestRunId,
                            TestRunTaskId = TestRunTaskId,
                            TestName = TestName,
                            TestData = TestData,
                            StatusId = (int) Status,
                            DateCreated = DateCreated,
                            DateExpires = DateExpires,
                            CallbackMethodName = CallbackMethodName,
                            CallbackClassName = CallbackClassName,
                            CallbackAssemblyName = CallbackAssemblyName
                        }
                    );
                }
            }
            catch (Exception)
            {
                // TODO: How are we handling exceptions here?
            }

            return id;
        }


        /// <summary>
        /// Saves the in-context AsyncTask object to the database.
        /// Determines update or insert based on presence of AsyncTaskId.
        /// </summary>
        public void Save()
        {
            if (AsyncTaskId > 0)
            {
                Update();
            }
            else
            {
                Insert();
            }
        }


        /// <summary>
        /// Updates the database record for the in-context AsyncTask object.
        /// </summary>
        private void Update()
        {
            try
            {
                using (var client = QuailServices.QuailDataServiceFactory.CreateClient())
                {
                    client.UpdateAsyncTask(
                        new QuailServices.QuailDataServiceReference.AsyncTask
                            {
                                AsyncTaskId = AsyncTaskId,
                                TestRunId = TestRunId,
                                TestRunTaskId = TestRunTaskId,
                                TestName = TestName,
                                TestData = TestData,
                                StatusId =  (int) Status,
                                DateCreated = DateCreated,
                                DateExpires = DateExpires,
                                CallbackMethodName = CallbackMethodName,
                                CallbackClassName = CallbackClassName,
                                CallbackAssemblyName = CallbackAssemblyName
                            }
                        );
                }
            }
            catch (Exception)
            {
                // TODO: How are we handling exceptions here?
            }
        }



        /// <summary>
        /// Update parent task status and message.
        /// </summary>
        /// <param name="url">URL of task running. Get this from Task.TestSession.</param>
        /// <param name="message">Message saved to parent task.</param>
        /// <param name="associatedTestRunId">Id of the finishing task to provide link in starting task.</param>
        private void UpdateParentTasks(string url, string message, int associatedTestRunId)
        {
            try
            {
                using (var client = QuailServices.QuailDataServiceFactory.CreateClient())
                {
                    // Start by updating the TestRuns table. First we get the current TestRun.
                    var testRun = client.GetTestRun(TestRunId);
                    var testRunStatus = (LogStatus) testRun.RunStatusId;

                    // We don't want to update this if another task has already set a bad status.
                    if (!testRunStatus.IsFailingStatus()) 
                    {
                        client.UpdateTestRun(
                            new QuailServices.QuailDataServiceReference.TestRun()
                            {
                                TestRunId = TestRunId,
                                RunStatusId = (int) Status
                            }
                        );
                    }

                    // Same goes for TestRunTask, except, here we also want to iterate up through all the TestRunTask's parents.
                    // But first start by updating the immediate parent.
                    var testRunTask = client.GetTestRunTaskById(TestRunTaskId);
                    LogStatus testRunTaskStatus = (LogStatus) testRunTask.TaskStatusId;


                    var startTaskId = TestRunTaskId;
                    // TODO: Linkage back to the finish task from the starting task. This may or may not be how to do it.
                    if (associatedTestRunId != 0)
                    {
                        client.InsertTestRunTaskLogEntry(startTaskId, (int) LogStatus.Information, url,
                                                         "AsyncFinish URL: " + BuildQuailUrl(associatedTestRunId),
                                                         DateTime.Now);
                    }

                    var parentupdated = false;
                    
                    // Here's where we start iterating.
                    do
                    {
                        if (!testRunTaskStatus.IsFailingStatus())
                        {
                            UpdateTestRunTaskStatus(testRunTask.TestRunTaskId);
                            if (testRunTask.ParentTestRunTaskId == 0)
                            {
                                parentupdated = true;
                            }
                            else
                            {
                                testRunTask = client.GetTestRunTaskById(testRunTask.ParentTestRunTaskId);
                                testRunTaskStatus = (LogStatus)testRunTask.TaskStatusId;
                            }
                        }
                        else
                        {
                            break;
                        }

                    } while (parentupdated == false);

                    // And finally insert a new record into TestRunTasks to show the work we've done.
                    client.InsertTestRunTaskLogEntry(startTaskId, (int)Status, url, message, DateTime.Now);
                }
            }
            catch (Exception)
            {
                // TODO: How are we handling exceptions here?
            }
        }

        private void UpdateTestRunTaskStatus(long testRunTaskId)
        {
            try
            {
                using (var client = QuailServices.QuailDataServiceFactory.CreateClient())
                {
                	client.UpdateTestRunTask(
                		new QuailServices.QuailDataServiceReference.TestRunTask
                			{
                				TestRunId = TestRunId,
                				TestRunTaskId = testRunTaskId,
                				TaskStatusId = (int) Status,
                			}
                		);
                }
            }
            catch (Exception)
            {
                // TODO: How are we handling exceptions here?
            }
        }


        /// <summary>
        /// Helper method constructs message saved to parent task describing outcome of async tasks processed.
        /// </summary>
        /// <param name="pendingAsyncTasks">List of AsyncTask tests.</param>
        /// <returns>HTML string containing outcome of provided AsyncTask tests.</returns>
        public static string BuildLogMessage(List<AsyncTask> pendingAsyncTasks)
        {
            var message = "No asynchronous tasks were pending this time.";

            if (pendingAsyncTasks.Count > 0)
            {
                message = string.Empty;

                foreach (LogStatus status in Enum.GetValues(typeof(LogStatus)))
                {
                    var asyncTasks = pendingAsyncTasks.Where(at => at.Status == status).ToList();

                    if (asyncTasks.Count > 0)
                    {
                        message += string.Format("<h3>{0}</h3>", status);
                        message += "<table border=1><thead>";
                        message += "<tr><th>Id</th><th>Name</th><th>Data</th><th>Created</th><th>Expires</th></tr>";
                        message += "</thead><tbody>";
                        foreach (AsyncTask asyncTask in asyncTasks)
                        {
                            message += string.Format("<tr><td>{0}</td><td><a href=\"{1}\">{2}</a></td><td>{3}</td><td>{4}</td><td>{5}</td></tr>",
                                asyncTask.AsyncTaskId,
                                asyncTask.QuailUrl,
                                asyncTask.TestName,
                                HttpUtility.HtmlEncode(asyncTask.TestData ?? ""),
                                asyncTask.DateCreated,
                                asyncTask.DateExpires
                            );
                        }
                        message += "</tbody></table><br/><br/>";
                    }
                }
            }

            return message;
        }
    }
}